# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do
  let(:variables) do
    Gitlab::Ci::Variables::Collection.new
      .append(key: 'PRESENT_VARIABLE', value: 'my variable')
      .append(key: 'PATH_VARIABLE', value: 'a/path/variable/value')
      .append(key: 'FULL_PATH_VARIABLE', value: '/a/full/path/variable/value')
      .append(key: 'EMPTY_VARIABLE', value: '')
      .to_hash
  end

  subject(:statement) do
    described_class.new(text, variables)
  end

  describe '.new' do
    context 'when variables are not provided' do
      it 'allows to properly initializes the statement' do
        statement = described_class.new('$PRESENT_VARIABLE')

        expect(statement.evaluate).to be_nil
      end
    end
  end

  describe '#evaluate' do
    using RSpec::Parameterized::TableSyntax

    subject(:evaluate) { statement.evaluate }

    where(:expression, :value) do
      '$PRESENT_VARIABLE == "my variable"'                          | true
      '"my variable" == $PRESENT_VARIABLE'                          | true
      '$PRESENT_VARIABLE == null'                                   | false
      '$EMPTY_VARIABLE == null'                                     | false
      '"" == $EMPTY_VARIABLE'                                       | true
      '$EMPTY_VARIABLE'                                             | ''
      '$UNDEFINED_VARIABLE == null'                                 | true
      'null == $UNDEFINED_VARIABLE'                                 | true
      '$PRESENT_VARIABLE'                                           | 'my variable'
      '$UNDEFINED_VARIABLE'                                         | nil
      "$PRESENT_VARIABLE =~ /var.*e$/"                              | true
      '$PRESENT_VARIABLE =~ /va\r.*e$/'                             | false
      '$PRESENT_VARIABLE =~ /va\/r.*e$/'                            | false
      "$PRESENT_VARIABLE =~ /var.*e$/"                              | true
      "$PRESENT_VARIABLE =~ /^var.*/"                               | false
      "$EMPTY_VARIABLE =~ /var.*/"                                  | false
      "$UNDEFINED_VARIABLE =~ /var.*/"                              | false
      "$PRESENT_VARIABLE =~ /VAR.*/i"                               | true
      '$PATH_VARIABLE =~ /path\/variable/'                          | true
      '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/' | true
      '$FULL_PATH_VARIABLE =~ /\\/path\\/variable\\/value$/'        | true
      '$PRESENT_VARIABLE != "my variable"'                          | false
      '"my variable" != $PRESENT_VARIABLE'                          | false
      '$PRESENT_VARIABLE != null'                                   | true
      '$EMPTY_VARIABLE != null'                                     | true
      '"" != $EMPTY_VARIABLE'                                       | false
      '$UNDEFINED_VARIABLE != null'                                 | false
      'null != $UNDEFINED_VARIABLE'                                 | false
      "$PRESENT_VARIABLE !~ /var.*e$/"                              | false
      "$PRESENT_VARIABLE !~ /^var.*/"                               | true
      '$PRESENT_VARIABLE !~ /^v\ar.*/'                              | true
      '$PRESENT_VARIABLE !~ /^v\/ar.*/'                             | true
      "$EMPTY_VARIABLE !~ /var.*/"                                  | true
      "$UNDEFINED_VARIABLE !~ /var.*/"                              | true
      "$PRESENT_VARIABLE !~ /VAR.*/i"                               | false

      '$PRESENT_VARIABLE && "string"'          | 'string'
      '$PRESENT_VARIABLE && $PRESENT_VARIABLE' | 'my variable'
      '$PRESENT_VARIABLE && $EMPTY_VARIABLE'   | ''
      '$PRESENT_VARIABLE && null'              | nil
      '"string" && $PRESENT_VARIABLE'          | 'my variable'
      '$EMPTY_VARIABLE && $PRESENT_VARIABLE'   | 'my variable'
      'null && $PRESENT_VARIABLE'              | nil
      '$EMPTY_VARIABLE && "string"'            | 'string'
      '$EMPTY_VARIABLE && $EMPTY_VARIABLE'     | ''
      '"string" && $EMPTY_VARIABLE'            | ''
      '"string" && null'                       | nil
      'null && "string"'                       | nil
      '"string" && "string"'                   | 'string'
      'null && null'                           | nil

      '$PRESENT_VARIABLE =~ /my var/ && $EMPTY_VARIABLE =~ /nope/' | false
      '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE'                 | 'my variable'
      '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE != "nope"'       | true
      '$PRESENT_VARIABLE && $EMPTY_VARIABLE'                       | ''
      '$PRESENT_VARIABLE && $UNDEFINED_VARIABLE'                   | nil
      '$UNDEFINED_VARIABLE && $EMPTY_VARIABLE'                     | nil
      '$UNDEFINED_VARIABLE && $PRESENT_VARIABLE'                   | nil

      '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ && $PATH_VARIABLE =~ /path\/variable/'      | true
      '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ && $PATH_VARIABLE =~ /path\/variable/'       | false
      '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ && $PATH_VARIABLE =~ /bad\/path\/variable/' | false
      '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ && $PATH_VARIABLE =~ /bad\/path\/variable/'  | false

      '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ || $PATH_VARIABLE =~ /path\/variable/'      | true
      '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ || $PATH_VARIABLE =~ /path\/variable/'       | true
      '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ || $PATH_VARIABLE =~ /bad\/path\/variable/' | true
      '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ || $PATH_VARIABLE =~ /bad\/path\/variable/'  | false

      '$PRESENT_VARIABLE =~ /my var/ || $EMPTY_VARIABLE =~ /nope/' | true
      '$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE'                 | true
      '$PRESENT_VARIABLE != "nope" || $EMPTY_VARIABLE == ""'       | true

      '$PRESENT_VARIABLE && null || $EMPTY_VARIABLE == ""'         | true
      '$PRESENT_VARIABLE || $UNDEFINED_VARIABLE'                   | 'my variable'
      '$UNDEFINED_VARIABLE || $PRESENT_VARIABLE'                   | 'my variable'
      '$UNDEFINED_VARIABLE == null || $PRESENT_VARIABLE'           | true
      '$PRESENT_VARIABLE || $UNDEFINED_VARIABLE == null'           | 'my variable'

      '($PRESENT_VARIABLE)'                                        | 'my variable'
      '(($PRESENT_VARIABLE))'                                      | 'my variable'
      '(($PRESENT_VARIABLE && null) || $EMPTY_VARIABLE == "")'     | true
      '($PRESENT_VARIABLE) && (null || $EMPTY_VARIABLE == "")'     | true
      '("string" || "test") == "string"'                           | true
      '(null || ("test" == "string"))'                             | false
      '("string" == ("test" && "string"))'                         | true
      '("string" == ("test" || "string"))'                         | false
      '("string" == "test" || "string")'                           | "string"
      '("string" == ("string" || (("1" == "1") && ("2" == "3"))))' | true
    end

    context 'with input references' do
      let(:variables) { { inputs: { 'cloud_provider' => 'aws', 'environment' => 'production' } } }

      where(:expression, :value) do
        '$[[ inputs.cloud_provider ]] == "aws"'                                                      | true
        '$[[ inputs.cloud_provider ]] == "gcp"'                                                      | false
        '$[[ inputs.cloud_provider ]] == "aws" && $[[ inputs.environment ]] == "production"'         | true
      end

      with_them do
        let(:text) { expression }

        it "evaluates to `#{params[:value].inspect}`" do
          expect(evaluate).to eq(value)
        end
      end
    end

    with_them do
      let(:text) { expression }

      it "evaluates to `#{params[:value].inspect}`" do
        expect(evaluate).to eq(value)
      end
    end
  end

  describe '#truthful?' do
    using RSpec::Parameterized::TableSyntax

    subject(:truthful?) { statement.truthful? }

    where(:expression, :value) do
      '$PRESENT_VARIABLE == "my variable"' | true
      "$PRESENT_VARIABLE == 'no match'"    | false
      '$UNDEFINED_VARIABLE == null'        | true
      '$PRESENT_VARIABLE'                  | true
      '$UNDEFINED_VARIABLE'                | false
      '$EMPTY_VARIABLE'                    | false
      '$INVALID = 1'                       | false
      "$PRESENT_VARIABLE =~ /var.*/"       | true
      "$UNDEFINED_VARIABLE =~ /var.*/"     | false
      "$PRESENT_VARIABLE !~ /var.*/"       | false
      "$UNDEFINED_VARIABLE !~ /var.*/"     | true
    end

    with_them do
      let(:text) { expression }

      it "returns `#{params[:value].inspect}`" do
        expect(truthful?).to eq value
      end
    end

    context 'when evaluating expression raises an error' do
      let(:text) { '$PRESENT_VARIABLE' }

      it 'returns false' do
        allow(statement).to receive(:evaluate)
          .and_raise(described_class::StatementError)

        expect(truthful?).to be_falsey
      end
    end

    context 'when variables have patterns' do
      let(:variables) do
        Gitlab::Ci::Variables::Collection.new
          .append(key: 'teststring', value: 'abcde')
          .append(key: 'pattern1', value: '/^ab.*/')
          .append(key: 'pattern2', value: '/^at.*/')
          .to_hash
      end

      where(:expression, :result) do
        '$teststring =~ "abcde"'     | true
        '$teststring =~ $teststring' | true
        '$teststring =~ $pattern1'   | true
        '$teststring =~ $pattern2'   | false
      end

      with_them do
        let(:text) { expression }

        it { is_expected.to eq(result) }
      end
    end
  end

  describe '#input_names' do
    subject(:input_names) { statement.input_names }

    context 'with simple input expression' do
      let(:text) { '$[[ inputs.cloud_provider ]] == "aws"' }

      it { is_expected.to eq(['cloud_provider']) }
    end

    context 'with multiple inputs using AND' do
      let(:text) { '$[[ inputs.a ]] == "x" && $[[ inputs.b ]] == "y"' }

      it { is_expected.to contain_exactly('a', 'b') }
    end

    context 'with multiple inputs using OR' do
      let(:text) { '$[[ inputs.env ]] == "prod" || $[[ inputs.env ]] == "stage"' }

      it 'deduplicates repeated inputs' do
        expect(input_names).to eq(['env'])
      end
    end

    context 'with nested expressions' do
      let(:text) { '($[[ inputs.a ]] == "x" && $[[ inputs.b ]] == "y") || $[[ inputs.c ]] == "z"' }

      it { is_expected.to contain_exactly('a', 'b', 'c') }
    end

    context 'with inputs and variables mixed' do
      let(:text) { '$[[ inputs.region ]] == "us" && $CI_COMMIT_BRANCH == "main"' }

      it 'returns only inputs, not variables' do
        expect(input_names).to eq(['region'])
      end
    end

    context 'with no inputs' do
      let(:text) { '$PRESENT_VARIABLE == "value"' }

      it { is_expected.to be_empty }
    end

    context 'with invalid expression' do
      let(:text) { 'invalid &&' }

      it 'returns empty array on error' do
        expect(input_names).to be_empty
      end
    end
  end
end
